package jmine.tec.utils.reflection.fast.asm;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;

import jmine.tec.utils.reflection.fast.FastClass;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Gera o bytecode de uma classe
 * 
 * @author takeshi
 */
public class FastClassGenerator implements Opcodes {

    private static final int ARGUMENTS_INDEX = 3;

    private static final AtomicLong UID = new AtomicLong();

    private final String generatedClassName;

    private final Class<?> targetType;

    private static final Method MULTI_METHOD, SINGLE_ARG_METHOD, NO_ARG_METHOD;

    private static final Method CTOR_METHOD;

    static {
        try {
            MULTI_METHOD =
                    GeneratedFastClassAccessor.class.getDeclaredMethod("invokeMethodMultipleArgs", int.class, Object.class, Object[].class);
            SINGLE_ARG_METHOD =
                    GeneratedFastClassAccessor.class.getDeclaredMethod("invokeMethodSingleArg", int.class, Object.class, Object.class);
            NO_ARG_METHOD = GeneratedFastClassAccessor.class.getDeclaredMethod("invokeMethodNoArgs", int.class, Object.class);
            CTOR_METHOD = GeneratedFastClassAccessor.class.getDeclaredMethod("invokeConstructor", int.class, Object[].class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * C'tor
     * 
     * @param type a classe para qual gerar o bytecode
     */
    public FastClassGenerator(Class<?> type) {
        this(generateName(), type);
    }

    /**
     * C'tor
     * 
     * @param generatedClassName o nome desejado para a classe, com os pacotes separados por '/'
     * @param targetType a classe para qual gerar o bytecode
     */
    public FastClassGenerator(String generatedClassName, Class<?> targetType) {
        super();
        this.generatedClassName = generatedClassName;
        this.targetType = targetType;
    }

    /**
     * Gera o bytecode
     * 
     * @return byte[]
     */
    public final byte[] emmitFastClassBytecode() {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        writer.visit(Opcodes.V1_5, ACC_PUBLIC + ACC_SUPER, this.generatedClassName, null,
                Type.getInternalName(GeneratedFastClassAccessor.class), new String[]{ Type.getInternalName(FastClass.class) });
        this.generateConstructor(writer);
        if (this.canInstance() && this.targetType.getConstructors().length > 0) {
            this.generateConstructorMethod(writer);
        }
        this.generateMultipleArgsMethod(writer);
        this.generateNoArgsMethods(writer);
        this.generateSingleArgsMethod(writer);
        writer.visitEnd();
        return writer.toByteArray();
    }

    /**
     * Verifica se o tipo pode ser instanciado
     * 
     * @return boolean
     */
    private boolean canInstance() {
        final int mod = this.targetType.getModifiers();
        if (this.targetType.isInterface()) {
            return false;
        }
        if (Modifier.isAbstract(mod)) {
            return false;
        }
        if (Modifier.isPrivate(mod) || Modifier.isProtected(mod)) {
            return false;
        }
        return !this.targetType.isAnonymousClass();
    }

    /**
     * @param writer {@link ClassWriter}
     */
    private void generateConstructorMethod(ClassWriter writer) {
        MethodVisitor visitor = BytecodeHelper.overwritePublicMethod(writer, CTOR_METHOD);
        visitor.visitCode();
        Constructor<?>[] ctors = this.targetType.getConstructors();
        final int max = ctors.length;
        Label[] gotos = this.createLabels(max);
        Label def = new Label();
        visitor.visitVarInsn(ILOAD, 1);
        visitor.visitTableSwitchInsn(0, max - 1, def, gotos);
        final String internalName = Type.getInternalName(this.targetType);
        for (int i = 0; i < gotos.length; i++) {
            visitor.visitLabel(gotos[i]);
            visitor.visitTypeInsn(NEW, internalName);
            visitor.visitInsn(DUP);
            BytecodeHelper.generateExpandArrayToTypes(visitor, 2, ctors[i].getParameterTypes());
            visitor
                    .visitMethodInsn(INVOKESPECIAL, Type.getInternalName(this.targetType), "<init>", Type
                            .getConstructorDescriptor(ctors[i]));
            visitor.visitInsn(ARETURN);
        }
        this.generateErrorMethodCalling(def, visitor);
        visitor.visitInsn(ARETURN);
        visitor.visitMaxs(0, 0);
        visitor.visitEnd();
    }

    /**
     * Cria os metodos com argumento unico
     * 
     * @param writer {@link ClassWriter}
     */
    private void generateSingleArgsMethod(ClassWriter writer) {
        Method[] methods = this.targetType.getMethods();
        Map<Integer, Method> withArity = BytecodeHelper.collectMethodsWithArity(methods, 1);
        if (withArity.isEmpty()) {
            return;
        }
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, SINGLE_ARG_METHOD);
        mv.visitCode();
        final int max = withArity.size();
        Label[] gotos = this.createLabels(max);
        int[] values = new int[max];
        Method[] toWrap = new Method[max];
        int j = 0;
        for (Entry<Integer, Method> entry : withArity.entrySet()) {
            values[j] = entry.getKey().intValue();
            toWrap[j++] = entry.getValue();
        }
        Label defaultLabel = new Label();

        mv.visitVarInsn(ILOAD, 1);
        mv.visitLookupSwitchInsn(defaultLabel, values, gotos);
        for (int i = 0; i < max; i++) {
            mv.visitLabel(gotos[i]);
            BytecodeHelper.generateSingleArgumentMethodCallReturnResult(mv, toWrap[i], 2, ARGUMENTS_INDEX);
        }
        this.generateErrorMethodCalling(defaultLabel, mv);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Gera os metodos que nao recebem argumentos
     * 
     * @param writer o {@link ClassWriter}
     */
    private void generateNoArgsMethods(ClassWriter writer) {
        Method[] methods = this.targetType.getMethods();
        Map<Integer, Method> selected = BytecodeHelper.collectMethodsWithArity(methods, 0);
        if (selected.isEmpty()) {
            return;
        }
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, NO_ARG_METHOD);
        mv.visitCode();
        final int max = selected.size();
        Label[] gotos = this.createLabels(max);
        int[] values = new int[max];
        Method[] toWrap = new Method[max];
        int j = 0;
        for (Entry<Integer, Method> entry : selected.entrySet()) {
            values[j] = entry.getKey().intValue();
            toWrap[j++] = entry.getValue();
        }
        Label defaultLabel = new Label();
        mv.visitVarInsn(ILOAD, 1);
        mv.visitLookupSwitchInsn(defaultLabel, values, gotos);
        final int targetIndex = 2;
        for (int i = 0; i < max; i++) {
            mv.visitLabel(gotos[i]);
            BytecodeHelper.generateNoArgMethodCallReturnResult(mv, toWrap[i], targetIndex);
        }
        this.generateErrorMethodCalling(defaultLabel, mv);
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Gera o metodo com invocacao multipla
     * 
     * @param writer {@link ClassWriter}
     */
    private void generateMultipleArgsMethod(ClassWriter writer) {
        MethodVisitor visitor = BytecodeHelper.overwritePublicMethod(writer, MULTI_METHOD);
        visitor.visitCode();
        Method[] methods = this.targetType.getMethods();
        final int size = methods.length;
        Label[] gotos = this.createLabels(size);
        final Label defaultLabel = new Label();
        // pushing the 'index' argument
        visitor.visitVarInsn(ILOAD, 1);
        // switch on the index
        visitor.visitTableSwitchInsn(0, methods.length - 1, defaultLabel, gotos);
        for (int i = 0; i < methods.length; i++) {
            this.generateAccessorForWithSwitch(methods[i], visitor, gotos[i]);
        }
        this.generateErrorMethodCalling(defaultLabel, visitor);
        visitor.visitInsn(ARETURN);
        visitor.visitMaxs(0, 0);
        visitor.visitEnd();
    }

    /**
     * Cria um array de labels
     * 
     * @param size int
     * @return Label[]
     */
    private Label[] createLabels(final int size) {
        Label[] gotos = new Label[size];
        for (int i = 0; i < size; i++) {
            gotos[i] = new Label();
        }
        return gotos;
    }

    /**
     * Gera a chamada ao metodo de erro
     * 
     * @param old {@link Label}
     * @param mv {@link MethodVisitor} host
     */
    private void generateErrorMethodCalling(Label old, MethodVisitor mv) {
        mv.visitLabel(old);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ILOAD, 1);
        mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(GeneratedFastClassAccessor.class), "error", "(I)Ljava/lang/Object;");
        mv.visitInsn(ACONST_NULL);
    }

    /**
     * Gera o construtor
     * 
     * @param cw {@link ClassWriter}
     */
    private void generateConstructor(ClassWriter cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitLdcInsn(Type.getType(this.targetType));
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(GeneratedFastClassAccessor.class), "<init>", "(Ljava/lang/Class;)V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * @param method the {@link Method}
     * @param mv the {@link MethodVisitor}
     * @param tgtLabel {@link Label}
     */
    private void generateAccessorForWithSwitch(Method method, MethodVisitor mv, Label tgtLabel) {
        mv.visitLabel(tgtLabel);
        BytecodeHelper.generateMethodCalling(mv, method, 2, ARGUMENTS_INDEX);
    }

    /**
     * @return {@link String}
     */
    public static String generateName() {
        final String preffix = "jmine/tec/generated/accessor/Acc";
        return generateName(preffix);
    }

    /**
     * Gera um nome com um prefixo
     * 
     * @param preffix {@link String}
     * @return {@link String}
     */
    public static String generateName(final String preffix) {
        return preffix + "_" + UID.getAndIncrement();
    }

    /**
     * @return the generatedClassName
     */
    public String getGeneratedClassName() {
        return this.generatedClassName.replaceAll("/", ".");
    }

    /**
     * @return the targetType
     */
    public Class<?> getTargetType() {
        return this.targetType;
    }

}
